PREEMPT_RT 抢占式和优先级调度


Linux 上的实时调度

开源软件社区主要采用两种方法将实时需求引入Linux


一般定义

这两种方法的目标都是在Linux多CPU的实时和非实时软件执行环境下,实现最低线程调度延迟


IA64 中断定义

中断可以描述为对硬件事件的立即响应。执行这种响应的过程通常称为中断服务例程(ISR)。在处理 ISR 的过程中,可能会产生多种延迟。这些延迟根据其来源分为两个部分:


Linux 多线程定义


通用 Linux 定时器定义

等时应用程序旨在确保任务在精确定义的时间点完成。然而,Linux 标准定时器通常无法满足所需的循环周期期限的分辨率或精度,甚至两者都不满足。

例如,Linux 中的典型定时器函数(如 gettimeofday() 系统调用)返回微秒级精度的时钟时间,而许多情况下需要的是纳秒级的定时器精度。

为了解决这一限制,创建了提供更高精度计时能力的额外 POSIX API

根据DIN 44300标准,实时操作被定义为系统能够在严格的时间要求内响应外部事件的能力。在实时系统中,处理数据和响应的时间是预先确定的,这对于确保系统可以及时地对关键事件做出反应非常关键。

这种类型的系统特别适用于那些对时间反应有严格要求的应用,例如自动控制系统、医疗监测设备和交通控制系统。在这些应用中,延迟或失败及时响应可能导致严重后果。

SCHED_FIFO 调度策略

先进先出(FIFO)策略,固定优先级,抢占式调度策略。如果你需要关于这一点的学习,你可以查阅这篇背景知识

当使用SCHED_FIFO时,调度器按优先级顺序扫描所有SCHED_FIFO线程的列表,并调度准备运行的最高优先级线程。

PREEMPT_RT 实时抢占与优先级调度

PREEMPT_RT 项目是由 Linux 内核开发者领导的开源框架,遵循 GPLv2 许可证

该项目的目标是逐步提升 Linux 内核对实时性要求的支持,并将这些改进合并到主线内核中。PREEMPT_RT 的开发与主线开发紧密协作,旨在实现更高效的实时性能。

多年来,在 PREEMPT_RT 项目中设计、开发和调试的许多改进现在已经成为主线 Linux 内核的一部分。这个项目是 Linux 内核的一个长期分支,最终目标是在所有改进都合并到主线后使其消失。


设置低延迟中断软件处理

PREEMPT_RT 通过在内核代码和众多驱动/模块代码库中推广 No non-threaded IRQ nesting 开发实践,强制执行基本的软件设计规则,以实现完全抢占和低延迟的调度。

设置 PREEMPT_RT 的优先级调度策略

标准的 Linux 内核包含多种调度策略,如 sched 的 manpage 中所描述的。以下三种策略与实时任务相关:

优先级继承假设锁(例如,spin_lock、mutex 等)会继承等待该锁的最高优先级进程线程的优先级。

PREEMPT_RT 为 rtmutexspin_lockmutex 代码提供了优先级继承功能。一个低优先级的进程可能持有一个高优先级进程所需的锁,从而有效地降低了高优先级进程的优先级。


chrt 调整运行时进程的 Linux 调度策略

在 Linux 上,chrt 命令可用于设置进程的实时属性,例如策略和优先级:

chrt --fifo --pid <priority> <pid>

以下命令将为 PID 为 9527 的进程设置调度属性为 SCHED_FIFO,并将优先级设为 99:

chrt --fifo --pid 99 9527
chrt -rr --pid <priority> <pid>

以下命令将为 PID 为 9527 的进程设置调度属性为 SCHED_RR,并将优先级设为 99:

chrt -rr --pid 99 1823
chrt --deadline --sched-runtime <nanoseconds> \
                  --sched-period <nanoseconds> \
                  --sched-deadline <nanoseconds> \
                  --pid <priority> <pid>

以下示例将为 PID 为 9527 的进程设置调度属性为 SCHED_DEADLINE。运行时间、截止时间和周期的单位为纳秒:

chrt --deadline --sched-runtime 1000000 \
                  --sched-period 5000000 \
                  --sched-deadline 2000000 \
                  --pid 0 9527
ps f -g 0 -o pid,policy,rtprio,cmd

将此信息汇总成一个表格:

优先级 名称
99 posixcputmr, migration
50 所有 IRQ 处理程序,除了 39-s-mmc042-s-mmc1。例如,367-enp2s0 处理一个网络接口
49 IRQ 处理程序 39-s-mmc042-s-mmc1
1 i915/signal, ktimersoftd, rcu_preempt, rcu_sched, rcub, rcuc
0 当前运行的其他任务

sched_setscheduler() 和 sched_setattr() 设置 Linux 进程的调度策略

sched_setscheduler 函数可用于更改线程的调度策略。以下值可用于设置实时调度策略

{{< callout type="warning" >}} 注意:非实时调度策略如 SCHED_OTHERSCHED_BATCHSCHED_IDLE 也是可用的。sched_setscheduler 函数不支持 SCHED_DEADLINE 调度策略。 {{< /callout >}}

sched_setscheduler 函数为实时线程策略设置 SCHED_FIFOSCHED_RR 调度策略及其优先级。

int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);

// 以下代码将配置正在运行的进程,以使用优先级为99的SCHED_RR调度:

struct sched_param param_rr;
memset(&param_rr, 0, sizeof(param_rr));
param_rr.sched_priority = 99;
pid_t pid = getpid();
if (sched_setscheduler(pid, SCHED_RR, &param_rr))
  perror("sched_setscheduler error:");

// 以下代码将配置正在运行的进程,以使用优先级为99的SCHED_FIFO调度:

struct sched_param param_fifo;
memset(&param_fifo, 0, sizeof(param_fifo));
param_fifo.sched_priority = 99;
pid_t pid = getpid();
if (sched_setscheduler(pid, SCHED_FIFO, &param_fifo))
  perror("sched_setscheduler error:");

POSIX API 设置调度器

步骤 1

要使用FIFO调度创建线程,请使用pthread_attr_init函数初始化pthread_attr_t(线程属性对象)对象:

pthread_attr_t attr_fifo;
pthread_attr_init(&attr_fifo);

步骤 2

初始化后,使用pthread_attr_setschedpolicyattr_fifo引用的线程属性对象设置为SCHED_FIFO(FIFO调度策略):

pthread_attr_setschedpolicy(&attr_fifo, SCHED_FIFO);

步骤 3

使用sched_param对象设置线程的优先级(可以为FIFO调度取1到99之间的值),并使用pthread_attr_setschedparam将参数值复制到线程属性:

struct sched_param param_fifo;
param_fifo.sched_priority = 92;
pthread_attr_setschedparam(&attr_fifo, &param_fifo);

步骤 4

设置线程属性的继承调度器属性。继承调度器属性决定新线程是从调用线程继承调度属性,还是从 attr 继承。要使用 attr 中定义的调度属性,需通过调用 pthread_attr_setinheritsched 函数并使用 PTHREAD_EXPLICIT_SCHED

pthread_attr_setinheritsched(&attr_fifo, PTHREAD_EXPLICIT_SCHED);

步骤 5

通过调用 pthread_create 函数创建线程:

pthread_t thread_fifo;
pthread_create(&thread_fifo, &attr_fifo, thread_function_fifo, NULL);

以下代码有助于在 FIFO 调度策略下实现最简单的可抢占多线程应用:

  #include <pthread.h>
  #include <stdio.h>

  void *thread_function_fifo(void *data) {
        printf("Inside Thread\n");
        return NULL;
}

int main(int argc, char* argv[]) {
        struct sched_param param_fifo;
        pthread_attr_t attr_fifo;
        pthread_t thread_fifo;
        int status = -1;
        memset(&param_fifo, 0, sizeof(param_fifo));
        status = pthread_attr_init(&attr_fifo);
        if (status) {
                printf("pthread_attr_init failed\n");
                return status;
        }
        status = pthread_attr_setschedpolicy(&attr_fifo, SCHED_FIFO);
        if (status) {
                printf("pthread_attr_setschedpolicy failed\n");
                return status;
        }
        param_fifo.sched_priority = 92;
        status = pthread_attr_setschedparam(&attr_fifo, &param_fifo);
        if (status) {
                printf("pthread_attr_setschedparam failed\n");
                return status;
        }
        status = pthread_attr_setinheritsched(&attr_fifo, PTHREAD_EXPLICIT_SCHED);
        if (status) {
                printf("pthread_attr_setinheritsched failed\n");
                return status;
        }
        status = pthread_create(&thread_fifo, &attr_fifo, thread_function_fifo, NULL);
        if (status) {
                printf("pthread_create failed\n");
                return status;
        }
        pthread_join(thread_fifo, NULL);
        return status;
}


内核线程 Read-Copy Update (RCU)

Read-Copy Update (RCU) API 在 Linux 代码中被广泛用于在线程同步中避免使用锁。这些 API 的特点如下:

在 RCU 读取端关键区中使用轻量级原语可以保证 RCU 保护的对象指针存在。

所有 RCU 写操作必须等待 RCU 的宽限期(grace period)结束,才能在将某些对象变为不可被读者访问后进行释放,并在此之后回收资源。

spinlock(&updater_lock);
q = cptr;
rcu_assign_pointer(cptr, new_p);
spin_unlock(&updater_lock);
synchronize_rcu(); /* Wait for grace period. */
kfree(q);

RCU 宽限期是为了让所有已存在的读取器完成它们的 RCU 读取端关键区操作。宽限期从调用 synchronize_rcu() 开始,直到所有 CPU 执行一次上下文切换后结束。


设置 POSIX 线程虚拟内存分配 (vma)

与标准 Linux 运行时相比,Linux 进程的内存管理在 PREEMPT_RT Linux 运行时中被视为一个重要且关键的方面。从内核调度的角度来看,进程和线程没有区别,它们都以类型为 running 的 task_struct 内核结构表示为任务。然而,从调度延迟的角度来看,进程上下文切换的时间显著长于同一进程内的用户线程上下文切换,因为进程切换需要刷新 TLB。

有不同的内存管理算法旨在优化可运行的进程并提高系统性能。例如,如果内核分配的 mmap() 返回的进程需要完整的内存页或仅需要部分内存页,内存管理将与调度器协同工作,以优化资源的使用。

探讨内存管理的三个主要领域:

内存锁定确保在关键时刻应用程序的内存页不会被从主内存中移除,这也能确保在实时关键操作中不会发生页面错误(page-fault),这一点非常重要。

应用程序中的每个线程都有自己的栈。可以通过 pthread 函数 pthread_attr_setstacksize() 来指定栈的大小。

pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize) 的语法:

如果栈的大小没有显式设置,则会分配默认的栈大小。如果应用程序使用大量的实时线程,建议使用比默认大小更小的栈,以节省资源并优化性能。

在实时(RT)线程执行时,不建议在实时关键路径中进行动态内存分配,因为这会增加发生页面错误的可能性。建议在实时执行开始之前分配所需的内存,并使用 mlock 或 mlockall 函数锁定内存。在以下示例中,线程函数尝试为线程的局部变量动态分配内存,并尝试访问存储在这些随机位置的数据。

在实时系统中,提前分配和锁定内存可以避免在关键操作期间由于内存分页引发的延迟,确保系统的实时性。

#define BUFFER_SIZE 1048576
void *thread_function_fifo(void *data) {
    double sum = 0.0;
    double* tempArray = (double*)calloc(BUFFER_SIZE, sizeof(double));
    size_t randomIndex;
    int i = 50000;
    while(i--)
    {
             randomIndex =  rand() % BUFFER_SIZE;
             sum += tempArray[randomIndex];
    }
             return NULL;
}

设置高分辨率时钟线程

Linux.org 社区逐步改进了定时器的精度,以提供一种更精确的方式唤醒系统并以更准确的时间间隔处理数据:

你可以通过下面的命令查看系统内定时器分辨率:

cat /proc/timer_list | grep 'cpu:\|resolution\|hres_active\|clock\|event_handler'

POSIX Linux 等时调度

等时应用程序会在固定的时间间隔后重复执行:

以下步骤概述了开发一个简单的等时实时线程(isoch-rt-thread)的基本过程,用于进行健全性检查:

  1. 定义线程属性,确保其为实时线程。
  2. 使用 POSIX pthread_create() 创建线程,配置其调度属性。
  3. 确保应用的执行时间短于指定的周期时间。

这类应用主要用于需要严格时间控制的场景,如音视频处理或工业自动化。

步骤 1

定义一个结构,该结构将包含时间段信息以及时钟的当前时间。此结构将用于在多个任务之间传递数据:

/*Data format to be passed between tasks*/
struct time_period_info {
        struct timespec next_period;
        long period_ns;

步骤 2

将循环线程的时间周期定义为1毫秒,并获取系统的当前时间:

/*Initialize the periodic task with 1ms time period*/
static void initialize_periodic_task(struct time_period_info *tinfo)
{
        /* keep time period for 1ms */
        tinfo->period_ns = 1000000;
        clock_gettime(CLOCK_MONOTONIC, &(tinfo->next_period));
}

步骤 3

使用计时器增量模块进行纳米睡眠,以完成真实线程的时间段:

/*Increment the timer until the time period elapses and the Real time task will execute*/
static void inc_period(struct time_period_info *tinfo)
{
      tinfo->next_period.tv_nsec += tinfo->period_ns;
      while(tinfo->next_period.tv_nsec >= 1000000000){
        tinfo->next_period.tv_sec++;
        tinfo->next_period.tv_nsec -=1000000000;
      }
}

步骤 4

使用循环等待时间段完成。假设与时间段相比,线程执行时间更短:

/*Assumption: Real time task requires less time to complete task as compared to period length, so wait till period completes*/
static void wait_for_period_complete(struct period_info *pinfo)
{
        inc_period(pinfo);
        /* Ignore possibilities of signal wakes */
        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &pinfo->next_period, NULL);
}

步骤 5

定义一个实时线程。为了简单起见,包括一个打印声明:

static void real_time_task()
{
        printf("Real-Time Task executing\n");
        return NULL;
}

步骤 6

初始化并触发实时线程循环执行。这将等待时间段的完成。此线程将作为POSIX线程从主线程创建。

void *realtime_isochronous_task(void *data)
{
        struct time_period_info tpinfo;
        periodic_task_init(&tpinfo);
        while (1) {
                real_time_task();
                wait_for_period_complete(&tpinfo);
        }
        return NULL;
}

一个非实时的主线程将在这里生成一个实时的等时应用程序线程。此外,它会设置抢占式调度的优先级和策略。

步骤 7

创建一个POSIX主线程,以创建和初始化具有属性的所有线程:

int main(int argc, char* argv[]) {
        struct sched_param param_fifo;
        pthread_attr_t attr_fifo;
        pthread_t thread_fifo;
        int status = -1;
        memset(&param_fifo, 0, sizeof(param_fifo));
        status = pthread_attr_init(&attr_fifo);
        if (status) {
                printf("pthread_attr_init failed\n");
                return status;
        }

接下来,使用FIFO调度策略设置实时线程:

status = pthread_attr_setschedpolicy(&attr_fifo, SCHED_FIFO);
if (status) {
  printf("pthread_attr_setschedpolicy failed\n");
  return status;
}

实时任务优先级设置为92。优先级可以在1到99之间:

param_fifo.sched_priority = 92;
status = pthread_attr_setschedparam(&attr_fifo, &param_fifo);
if (status) {
        printf("pthread_attr_setschedparam failed\n");
        return status;
}

设置线程属性的inherit-scheduler属性。inherit-scheduler属性决定了新线程是从调用线程还是从attr中取调度属性:

status = pthread_attr_setinheritsched(&attr_fifo, PTHREAD_EXPLICIT_SCHED);
if (status) {
        printf("pthread_attr_setinheritsched failed\n");
        return status;
}

以下代码创建实时等时应用程序线程:

status = pthread_create(&thread_fifo, &attr_fifo, realtime_isochronous_task, NULL);
if (status) {
        printf("pthread_create failed\n");
        return status;
}

等待实时任务完成:

        pthread_join(thread_fifo, NULL);
    return status;
}

{{% --- title="点击展开完整示例" closed="true" %}}

/*Header Files*/
#include <pthread.h>
#include <stdio.h>
#include <string.h>

/*Data format to be passed between tasks*/
struct time_period_info {
     struct timespec next_period;
     long period_ns;
};

/*Initialize the periodic task with 1ms time period*/
static void initialize_periodic_task(struct time_period_info *tinfo){
     /*Keep time period for 1ms*/
     tinfo->period_ns = 1000000;
     clock_gettime(CLOCK_MONOTONIC, &(tinfo->next_period));
}

/*Increment the timer to till time period elapsed*/
static void inc_period(struct time_period_info *tinfo){
     tinfo->next_period.tv_nsec += tinfo->period_ns;
     while(tinfo->next_period.tv_nsec >= 1000000000){
             tinfo->next_period.tv_sec++;
             tinfo->next_period.tv_nsec -=1000000000;
     }
}

/*Real time task requires less time to complete task as compared to period length, so wait till period completes*/
static void wait_for_period_complete(struct time_period_info *tinfo){
     inc_period(tinfo);
     clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &tinfo->next_period, NULL);
}

/*Real Time Task*/
static void* real_time_task(){
     printf("Real-Time Task executing\n");
     return NULL;
}

/*Main module for an isochronous application task with Real Time priority and scheduling call as SCHED_FIFO */
void *realtime_isochronous_task(void *data){

     struct time_period_info tinfo;
     initialize_periodic_task(&tinfo);

     while(1){
             real_time_task();
             wait_for_period_complete(&tinfo);
     }
     return NULL;
}

/*Non Real Time master thread that will spawn a Real Time isochronous application thread*/
int main(int argc, char* argv[]) {

     struct sched_param param_fifo;
     pthread_attr_t attr_fifo;
     pthread_t thread_fifo;
     int status = -1;
     memset(&param_fifo, 0, sizeof(param_fifo));

     status = pthread_attr_init(&attr_fifo);
     if (status) {
             printf("pthread_attr_init failed\n");
             return status;
     }

     status = pthread_attr_setschedpolicy(&attr_fifo, SCHED_FIFO);
     if (status) {
             printf("pthread_attr_setschedpolicy failed\n");
             return status;
     }

     param_fifo.sched_priority = 92;
     status = pthread_attr_setschedparam(&attr_fifo, &param_fifo);
     if (status) {
             printf("pthread_attr_setschedparam failed\n");
             return status;
     }

     status = pthread_attr_setinheritsched(&attr_fifo, PTHREAD_EXPLICIT_SCHED);
     if (status) {
             printf("pthread_attr_setinheritsched failed\n");
             return status;
     }

     status = pthread_create(&thread_fifo, &attr_fifo, realtime_isochronous_task, NULL);
     if (status) {
             printf("pthread_create failed\n");
             return status;
     }

     pthread_join(thread_fifo, NULL);
     return status;
}